package standardize;

import parser.CodeReviewToken;
import parser.JavaTokenTypes;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Formats a list of tokens in a standard way
 * The could probably be better done using the AST and StringTemplate
 *
 * @author begge
 * @license http://www.freebsd.org/copyright/license.html  BSD License (4 Clause)
 * Date: May 18, 2007
 */
public class FragmentFormatter {
    private FragmentFormatter() {
    }

    public static String getStandardizedText(List<CodeReviewToken> tokens) {

        StringBuffer sb = new StringBuffer();

        // figure out where to start the tab indent
        int tabs = initialIndent(tokens);
        if (tabs < 0) {
            // there are more closing tabs than opening, so we need to pre-indent this amount.
            tabs = -tabs;
        } else {
            tabs = 0;
        }
        int lastType = 0;
        boolean inFor = false;
        int parens = 0;
        for (int i = 0; i < tokens.size(); i++) {
            CodeReviewToken token = tokens.get(i);
            int nextType;
            if (i < tokens.size() - 1) {
                nextType = tokens.get(i + 1).getType();
            } else {
                nextType = 0;
            }

            int type = token.getType();

            tabs = adjustIndent(type, tabs);
            if (needsIndent(lastType, inFor, i, type)) {
                repeat(tabs, "\t", sb);
            }

            if (type == JavaTokenTypes.LITERAL_for) {
                inFor = true;
                parens = 0;
            }
            if (type == JavaTokenTypes.LPAREN) {
                parens++;
            } else if (type == JavaTokenTypes.RPAREN) {
                parens--;
                inFor &= parens > 0;
            }

            if (before(lastType, type)) {
                sb.append(' ');
            }
            sb.append(token.getText());
            if (after(type, nextType) || inFor && type == JavaTokenTypes.SEMI) {
                sb.append(' ');
            }

            if (newline(type) && !inFor) {
                sb.append("\n");
            }
            lastType = type;
        }
        return sb.toString().intern();
    }

    private static boolean needsIndent(final int lastType, final boolean inFor, final int i, final int type) {
        return newline(lastType) && (!inFor || i == 0 && !(type == JavaTokenTypes.LCURLY || type == JavaTokenTypes.SLIST));
    }

    private static int adjustIndent(final int type, int tabs) {
        if (type == JavaTokenTypes.LCURLY || type == JavaTokenTypes.SLIST) {
            tabs++;
        }
        if (type == JavaTokenTypes.RCURLY) {
            tabs--;
        }
        return tabs;
    }

    private static int initialIndent(final List<CodeReviewToken> tokens) {
        int tabs = 0;
        for (CodeReviewToken token : tokens) {
            int type = token.getType();
            tabs = adjustIndent(type, tabs);
        }
        return tabs;
    }

    private static void repeat(int count, String s, StringBuffer sb) {
        for (int i = 0; i < count; i++) {
            sb.append(s);
        }
    }

    private static final Set<Integer> BEFORE = new HashSet<Integer>(Arrays.asList(JavaTokenTypes.LITERAL_throws));

    private static boolean before(final int lastType, int type) {
        return BEFORE.contains(type) || both(type) || type == JavaTokenTypes.SLIST && lastType != 0;
    }

    private static final Set<Integer> BOTH = new HashSet<Integer>(Arrays.asList(JavaTokenTypes.EQUAL,
            JavaTokenTypes.ASSIGN,
            JavaTokenTypes.GT,
            JavaTokenTypes.LT,
            JavaTokenTypes.GE,
            JavaTokenTypes.LE,
            JavaTokenTypes.PLUS_ASSIGN,
            JavaTokenTypes.MINUS_ASSIGN,
            JavaTokenTypes.STAR_ASSIGN,
            JavaTokenTypes.DIV_ASSIGN,
            JavaTokenTypes.MOD_ASSIGN,
            JavaTokenTypes.SR_ASSIGN,
            JavaTokenTypes.BSR_ASSIGN,
            JavaTokenTypes.SL_ASSIGN,
            JavaTokenTypes.BAND_ASSIGN,
            JavaTokenTypes.BXOR_ASSIGN,
            JavaTokenTypes.BOR_ASSIGN,
            JavaTokenTypes.LOR,
            JavaTokenTypes.LAND,
            JavaTokenTypes.BOR,
            JavaTokenTypes.BXOR,
            JavaTokenTypes.NOT_EQUAL,
            JavaTokenTypes.LITERAL_instanceof
    ));

    private static boolean both(final int type) {
        return BOTH.contains(type);
    }

    private static final Set<Integer> AFTER = new HashSet<Integer>(Arrays.asList(JavaTokenTypes.EQUAL,
            JavaTokenTypes.ASSIGN,
            JavaTokenTypes.LITERAL_private,
            JavaTokenTypes.LITERAL_public,
            JavaTokenTypes.LITERAL_protected,
            JavaTokenTypes.LITERAL_transient,
            JavaTokenTypes.LITERAL_native,
            JavaTokenTypes.LITERAL_threadsafe,
            JavaTokenTypes.LITERAL_synchronized,
            JavaTokenTypes.LITERAL_volatile,
            JavaTokenTypes.LITERAL_void,
            JavaTokenTypes.LITERAL_boolean,
            JavaTokenTypes.LITERAL_byte,
            JavaTokenTypes.LITERAL_char,
            JavaTokenTypes.LITERAL_short,
            JavaTokenTypes.LITERAL_int,
            JavaTokenTypes.LITERAL_float,
            JavaTokenTypes.LITERAL_long,
            JavaTokenTypes.LITERAL_double,
            JavaTokenTypes.LITERAL_throws,
            JavaTokenTypes.LITERAL_new,
            JavaTokenTypes.LITERAL_if,
            JavaTokenTypes.LITERAL_for
    ));

    private static boolean after(final int type, final int next) {
        boolean b = AFTER.contains(type);
        b |= type == JavaTokenTypes.IDENT && next == JavaTokenTypes.IDENT;
        b |= type == JavaTokenTypes.LITERAL_return && next == JavaTokenTypes.IDENT;
        b |= both(type);
        return b;
    }

    private static final Set<Integer> NEWLINE = new HashSet<Integer>(Arrays.asList(JavaTokenTypes.LCURLY,
            JavaTokenTypes.RCURLY,
            JavaTokenTypes.SEMI,
            JavaTokenTypes.SLIST
    ));

    private static boolean newline(int type) {
        return NEWLINE.contains(type);
    }

}
